package uk.ac.aber.paws.client.updater;

/**
 * @author Rob Johnson, Aberystwyth University
 * @email rob.johnson@aber.ac.uk
 * @date 16th August 2011
 * 
 * UpdateManager.java
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.logging.Level;

import uk.ac.aber.paws.client.config.ConfigManager;
import uk.ac.aber.paws.client.core.Initiator;
import uk.ac.aber.paws.client.core.Operations;
import uk.ac.aber.paws.client.core.PAWSClient;
import uk.ac.aber.paws.client.db.*;
import uk.ac.aber.paws.client.util.*;
import java.security.*;
import java.io.*;
import java.net.*;

/**
 * This class deals with all the JAR file update operations and configuration updates.
 * @author Rob Johnson
 *
 */
public class UpdateManager {

	/**
	 * Flag used for determining whether an update check is being made with the server. It prevents the system 
	 * being updated while this check is in place.
	 */
	private static boolean UPDATING = false;
	
	public static boolean checkForNewUpdates() {
		
		boolean restartRequired = false;
		
		try {
			// get the update definition file
			boolean downloadResult = downloadFile(new URL(
					PAWSClient.getConfig().getProperty("app.update.updatelocation")+
					PAWSClient.getConfig().getProperty("app.update.updatedeffile")),
					
					PAWSClient.getConfig().getProperty("app.main.homedir")+
					PAWSClient.getConfig().getProperty("app.main.updatedir")+
					PAWSClient.getConfig().getProperty("app.update.updatedeffile")
					);
			
			if (downloadResult) {
				
				// parse this file into data structure, should be in format: file name,checksum
				File updateDefFile = new File(PAWSClient.getConfig().getProperty("app.main.homedir")+
						PAWSClient.getConfig().getProperty("app.main.updatedir")+
						PAWSClient.getConfig().getProperty("app.update.updatedeffile"));
				
				if (updateDefFile.exists()) {
					
					 BufferedReader in = new BufferedReader(new FileReader(updateDefFile));
					 String line;
					 
					 while ((line = in.readLine()) != null) {
						 
						 StringTokenizer st = new StringTokenizer(line,",");
						 
						 if (st.countTokens() == 3) {
						 	  
							 String folder = (String)st.nextElement();
							 String filename = (String)st.nextElement();
							 String checksum = (String)st.nextElement();
							 
							 String localChecksum = getLocalChecksum(new File(PAWSClient.getConfig().getProperty("app.main.homedir")+folder+"/"+filename));
							 
							 // check the checksum of the current file with the definition derived from the server
							 if (!checksum.equals(localChecksum)) {
								
								 if (new File(PAWSClient.getConfig().getProperty("app.main.homedir")+
											PAWSClient.getConfig().getProperty("app.main.updatedir")+
											filename).exists()) {
									 new File(PAWSClient.getConfig().getProperty("app.main.homedir")+
										PAWSClient.getConfig().getProperty("app.main.updatedir")+
										filename).delete();
								 }
								 
								 downloadResult = downloadFile(new URL(
											PAWSClient.getConfig().getProperty("app.update.updatelocation")+
											filename),
											
											PAWSClient.getConfig().getProperty("app.main.homedir")+
											PAWSClient.getConfig().getProperty("app.main.updatedir")+
											filename
											);
								
								 if (downloadResult) {
									 
									 // if downloaded file equals the checksum specified...
									 if (getLocalChecksum(new File(PAWSClient.getConfig().getProperty("app.main.homedir")+
												PAWSClient.getConfig().getProperty("app.main.updatedir")+filename)).equals(checksum)) {
										 
										 
										// ok...now perform an update
										Initiator.LOG_FILE.log(Level.INFO,"New PAWS update file "+filename+" has been downloaded and verified successfully");
										
										commitDownload(PAWSClient.getConfig().getProperty("app.main.homedir")+
															PAWSClient.getConfig().getProperty("app.main.updatedir")+
															filename,
									
															PAWSClient.getConfig().getProperty("app.main.homedir")+
															folder+"/"+filename);
										
										restartRequired = true;
									 }
								 }
							 }
							 else {
								 
								 Initiator.LOG_FILE.log(Level.INFO,filename+" is up-to-date with checksum "+checksum);
							 }
						}
					}
				}
			}
		}
		catch (MalformedURLException e) {	
		}
		catch (FileNotFoundException e) {
		}
		catch (IOException e) {
		}
		
		return restartRequired;
	}
	
	private static boolean downloadFile(URL url, String localFile) {
		
		Initiator.LOG_FILE.log(Level.INFO,"URL = "+url.toString());
		Initiator.LOG_FILE.log(Level.INFO,"Local file = "+localFile);
		
		try {
		    URLConnection uc = url.openConnection();
		   
		    int contentLength = uc.getContentLength();
		    
		    InputStream inputStream = uc.getInputStream();
		    InputStream in = new BufferedInputStream(inputStream);
		    
		    byte[] data = new byte[contentLength];
		    int bytesRead = 0;
		    int offset = 0;
		    
		    while (offset < contentLength) {
		    	
		    	bytesRead = in.read(data, offset, data.length - offset);
		    	
		    	if (bytesRead == -1) {
		    		break;
		    	}
		    	
		    	offset += bytesRead;
		    }
		    
		    in.close();

		    File updateFile = new File(localFile);
		    
		    updateFile.mkdirs();
		    
		    if (updateFile.exists()) {
		    	updateFile.delete();
		    }
		    
		    FileOutputStream out = new FileOutputStream(updateFile);
		    
		    out.write(data);
		    out.flush();
		    out.close();
		    
		    return true;
		}
		catch (IOException ex) {
			Initiator.LOG_FILE.log(Level.ALL,ex.toString());
		}
		
		return false;
	}
	
	/**
	 * @return Returns the path to the currently active JAR file from which this application is running.
	 */
	private static File getThisJarPath() {
		
		Class<UpdateManager> cls = UpdateManager.class;

		ProtectionDomain pDomain = cls.getProtectionDomain();
		CodeSource cSource = pDomain.getCodeSource();
		URL loc = cSource.getLocation(); 
		
		File f = null;
		
		try {
			f = new File(loc.toURI());
		} 
		catch (URISyntaxException e) {
			f = new File(loc.getPath());
		}
		
		return f;
	}
	
	/**
	 * This function calculates the local checksum of the runtime jar and the server based update that
	 * is in place on the management server, compares the two and returns the compare result.
	 * 
	 *  @return Returns true if the server update is a new version or false if running latest version.
	 */
	public static boolean checkForJarUpdate() throws NullPointerException,FileNotFoundException {
		
		// stop the other process from trying to update the downloaded file
		UPDATING = true;
		
		File updateFile = new File(PAWSClient.getConfig().getProperty("app.main.homedir")+PAWSClient.getConfig().getProperty("app.main.updatedir")+PAWSClient.getConfig().getProperty("app.main.pawsclientname"));
		
		// delete existing update file if it exists
		if (updateFile.exists()) {
			updateFile.delete();
		}
		
		String localJarChecksum = getLocalChecksum(getThisJarPath());
		
		
		Initiator.LOG_FILE.log(Level.CONFIG,"Checksum of current JAR file: "+localJarChecksum);
		
		Initiator.LOG_FILE.log(Level.CONFIG,"Update file location: "+
						PAWSClient.getConfig().getProperty("app.update.updatelocation")+
						PAWSClient.getConfig().getProperty("app.main.pawsclientname"));
		
		String serverJarChecksum = null;
		
		try {
			serverJarChecksum = getServerChecksum(new URL(
					PAWSClient.getConfig().getProperty("app.update.updatelocation")+
					PAWSClient.getConfig().getProperty("app.main.pawsclientname")+
					PAWSClient.getConfig().getProperty("app.main.checksumkey")
					));
		} 
		catch (MalformedURLException e) {
			Initiator.LOG_FILE.log(Level.ALL,e.toString());	
		}
		
		Initiator.LOG_FILE.log(Level.CONFIG,"Checksum of server JAR file: "+serverJarChecksum);
		
		if (serverJarChecksum != null && localJarChecksum != null) {
			
			if (!localJarChecksum.equals(serverJarChecksum)) {
				Initiator.LOG_FILE.log(Level.INFO,"New PAWS Client update is available");
				
				try {
					// download the file
					downloadFile(new URL(
							PAWSClient.getConfig().getProperty("app.update.updatelocation")+
							PAWSClient.getConfig().getProperty("app.main.pawsclientname")),
							
							PAWSClient.getConfig().getProperty("app.main.homedir")+
							PAWSClient.getConfig().getProperty("app.main.updatedir")+
							PAWSClient.getConfig().getProperty("app.main.pawsclientname")
							);
					
					// check that the new download file checksum is correct
					if (getLocalChecksum(new File(PAWSClient.getConfig().getProperty("app.main.updatefile"))).equals(serverJarChecksum)) {
						
						// ok...now perform an update
						Initiator.LOG_FILE.log(Level.INFO,"New PAWS Client update has been downloaded and verified successfully");
						
						commitDownload(PAWSClient.getConfig().getProperty("app.main.homedir")+
											PAWSClient.getConfig().getProperty("app.main.updatedir")+
											PAWSClient.getConfig().getProperty("app.main.pawsclientname"),
											
											PAWSClient.getConfig().getProperty("app.main.homedir")+
											PAWSClient.getConfig().getProperty("app.main.libdir")+
											PAWSClient.getConfig().getProperty("app.main.pawsclientname"));
					}
					else {
						// the server update downloaded does not match the checksum that was advertised by the server
						Initiator.LOG_FILE.log(Level.CONFIG,"Checksum mismatch for update downloaded from server");
						
					}
				} 
				catch (MalformedURLException e) {
				
					Initiator.LOG_FILE.log(Level.INFO,e.toString());
				}
				
				return true;
			}
			else {
				
				Initiator.LOG_FILE.log(Level.CONFIG,"No new update available from server.");
			}
		}
		
		UPDATING = false;
		
		return false;
	}

	public static boolean checkForUpdate(UpdateDetails details) {
		
		// stop the other process from trying to update the downloaded file
		UPDATING = true;
		
		String localChecksum = getLocalChecksum(new File(details.getSourceFile()));
		
		String serverChecksum = null;
		
		try {
			serverChecksum = getServerChecksum(new URL(details.getUpdateFileChecksum()));
		} 
		catch (MalformedURLException e) {	
		}
		
		if (serverChecksum != null && !serverChecksum.equals("update off")) {
			
			if (!localChecksum.equals(serverChecksum)) {
				return true;
			}
			else {
				return false;
			}
		}
		
		return false;
		
		
	}


	public static boolean updateNow(UpdateDetails details) {
		
		if (checkForUpdate(details)) {
			
			try {
				String serverChecksum = getServerChecksum(new URL(details.getUpdateFileChecksum()));
			
				// download the file
				downloadFile(new URL(details.getUpdateFile()),details.getTemporaryFile());
				
				// check that the new download file checksum is correct
				if (getLocalChecksum(new File(details.getTemporaryFile())).equals(serverChecksum)) {
					
					try {
						FileCopy.copyFile(new File(details.getTemporaryFile()),new File(details.getSourceFile()));
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//commitDownload(details.getTemporaryFile(),details.getSourceFile());
					
					return true;
				
				}
				else {
					new File(details.getTemporaryFile()).delete();
				}
			
			} 
			catch (MalformedURLException e) {	
			}
			
		}
		
		return false;
	}
	
	private static void commitDownload(String updateFile, String runFile) {
	
		try {
			if (FileCopy.copyFile(new File(updateFile),new File(runFile))) {
				Initiator.LOG_FILE.log(Level.INFO,"Successfully updated the PAWS Client software");
				Operations.cleanCloseDown(true);
			}
			else {
				Initiator.LOG_FILE.log(Level.INFO,"Failed to commit new PAWS Client software");
			}
		}
		catch (Exception ex) {
			Initiator.LOG_FILE.log(Level.CONFIG,ex.toString());
		}
		
		
	}
	
	public static String getLocalChecksum(File theFile) {
		
		try {
				
			MessageDigest md = MessageDigest.getInstance("MD5"); 
			FileInputStream is = new FileInputStream(theFile);
			
			StringBuffer buf = new StringBuffer();
			
			int curChar=0;
			byte[] store = new byte[1024];
			
			try {
				while ((curChar = is.read(store)) != -1 ) {
						buf.append((char)curChar);
				}
			}
			catch (IOException ex) {
				Initiator.LOG_FILE.log(Level.CONFIG,ex.toString());
			}
					
			try {
				md.update(buf.toString().getBytes("UTF-8"));
			} 
			finally { 
			  is.close(); 
			} 
			byte[] digest = md.digest();
			
			StringBuffer hexString = new StringBuffer();
			
			for (int i=0;i<digest.length;i++) {
				hexString.append(Integer.toHexString(0xFF & digest[i]));
			}

			return hexString.toString();
		}
		catch (Exception ex) {
			ex.printStackTrace();
		}
		
		return null;
	}

	/**
	 * Retrieves the checksum from a URL for a given file
	 * @param theFile The file object to be calculated
	 * @return The checksum string
	 */
	private static String getServerChecksum(URL theChecksumFile) {
		
		String inputLine = null;
		
		try {
				
			BufferedReader in = new BufferedReader(new InputStreamReader(theChecksumFile.openStream()));

			inputLine = in.readLine();
			 
			in.close();

		}
		catch (Exception ex) {
			ex.printStackTrace();
		}
		
		return inputLine;
	}
	
}
